import 'dart:convert';
import 'dart:math';

import '../logger.dart';
import 'package:intl/intl.dart';

import 'log_printer.dart';

class PrettyPrinter implements LogPrinter {
  static const topLeftCorner = '┌';
  static const bottomLeftCorner = '└';
  static const middleCorner = '├';
  static const verticalLine = '│';
  static const doubleDivider = '─';
  static const singleDivider = '┄';

  static final levelColors = {
    LoggerLevel.DEBUG: AnsiColor.none(),
    LoggerLevel.INFO: AnsiColor.fg(12),
    LoggerLevel.WARN: AnsiColor.fg(208),
    LoggerLevel.ERROR: AnsiColor.fg(196),
  };

  static final levelEmojis = {
    LoggerLevel.DEBUG: '🐛 ',
    LoggerLevel.INFO: '💡 ',
    LoggerLevel.WARN: '⚠️ ',
    LoggerLevel.ERROR: '⛔ ',
  };
  final int stackTraceBeginIndex;
  final int startMethodCount;
  final int endMethodCount;
  final int lineLength;
  final bool colors;
  final bool printEmojis;
  final bool printTime;
  final int limitLength;
  final bool showLogName;
  final bool showLevelName;
  final Map<LoggerLevel, bool> excludeBox;
  final bool noBoxingByDefault;
  late final Map<LoggerLevel, bool> includeBox;

  /// Matches a stacktrace line as generated on Android/iOS devices.
  /// For example:
  /// #1      Logger.log (package:logger/src/logger.dart:115:29)
  static final _deviceStackTraceRegex =
      RegExp(r'#[0-9]+[\s]+(.+) \(([^\s]+)\)');

  /// Matches a stacktrace line as generated by Flutter web.
  /// For example:
  /// packages/logger/src/printers/pretty_printer.dart 91:37
  static final _webStackTraceRegex =
      RegExp(r'^((packages|dart-sdk)\/[^\s]+\/)');

  /// Matches a stacktrace line as generated by browser Dart.
  /// For example:
  /// dart:sdk_internal
  /// package:logger/src/logger.dart
  static final _browserStackTraceRegex =
      RegExp(r'^(?:package:)?(dart:[^\s]+|[^\s]+)');

  String _topBorder = '';
  String _middleBorder = '';
  String _bottomBorder = '';

  PrettyPrinter({
    this.showLogName = false,
    this.showLevelName = false,
    this.stackTraceBeginIndex = 0,
    this.startMethodCount = 0,
    this.endMethodCount = 2,
    this.lineLength = 120,
    this.colors = true,
    this.printEmojis = true,
    this.printTime = false,
    this.limitLength = 800,
    this.excludeBox = const {},
    this.noBoxingByDefault = false,
  }) {
    var doubleDividerLine = StringBuffer();
    var singleDividerLine = StringBuffer();
    for (var i = 0; i < lineLength - 1; i++) {
      doubleDividerLine.write(doubleDivider);
      singleDividerLine.write(singleDivider);
    }

    _topBorder = '$topLeftCorner$doubleDividerLine';
    _middleBorder = '$middleCorner$singleDividerLine';
    _bottomBorder = '$bottomLeftCorner$doubleDividerLine';

    includeBox = {};
    for (var l in LoggerLevel.values) {
      includeBox[l] = !noBoxingByDefault;
    }
    excludeBox.forEach((k, v) => includeBox[k] = !v);
  }

  @override
  List<String> log(LogEvent event) {
    var messageStr = stringifyMessage(event.message);
    String? stackTraceStr;
    if (endMethodCount > 0 && startMethodCount >= 0 && event.stackTrace != StackTrace.empty) {
        stackTraceStr = formatStackTrace(event.stackTrace, startMethodCount, endMethodCount);
    }
    var errorStr = event.error?.toString();

    String? timeStr;
    if (printTime) {
      timeStr = getTime();
    }
    return _formatAndPrint(
      getLabel(event),
      event.level,
      messageStr,
      timeStr,
      errorStr,
      stackTraceStr,
    );
  }

  String getLabel(LogEvent event) {
    final sb = new StringBuffer();
    // if (showLogName) {
    //   final name = event.name;
    //   if (name != null && name.isNotEmpty) {
    //     sb.write("[${name}]");
    //   }
    // }
    if (showLevelName) {
      final levelLabel = getLevelLabel(event);
      if (levelLabel.isNotEmpty) {
        sb.write("[${levelLabel}]");
      }
    }
    return sb.toString();
  }

  String getLevelLabel(LogEvent event) {
    switch (event.level) {
      case LoggerLevel.WARN:
        return "WARN";
      case LoggerLevel.ERROR:
        return "ERROR";
      case LoggerLevel.INFO:
        return "INFO";
      case LoggerLevel.DEBUG:
        return "DEBUG";
      case LoggerLevel.NONE:
        return "";
    }
  }

  String stringifyMessage(dynamic message) {
    final finalMessage = message is Function ? message() : message;
    if (finalMessage is Map || finalMessage is Iterable) {
      var encoder = JsonEncoder.withIndent('  ', toEncodableFallback);
      return encoder.convert(finalMessage);
    } else {
      return finalMessage.toString();
    }
  }

  Object toEncodableFallback(dynamic object) {
    return object.toString();
  }

  String? formatStackTrace(StackTrace? stackTrace, int startMethodCount,int endMethodCount) {
    var lines = stackTrace.toString().split('\n');
    if (stackTraceBeginIndex > 0 && stackTraceBeginIndex < lines.length - 1) {
      lines = lines.sublist(stackTraceBeginIndex);
    }
    var formatted = <String>[];
    var count = 0;
    for (var line in lines) {
      if (_discardDeviceStacktraceLine(line) ||
          _discardWebStacktraceLine(line) ||
          _discardBrowserStacktraceLine(line) ||
          line.isEmpty) {
        continue;
      }
      if(count > startMethodCount) {
        formatted.add('#$count   ${line.replaceFirst(RegExp(r'#\d+\s+'), '')}');
      }
      if (++count == endMethodCount) {
        break;
      }
    }

    if (formatted.isEmpty) {
      return null;
    } else {
      return formatted.join('\n');
    }
  }

  bool _discardDeviceStacktraceLine(String line) {
    var match = _deviceStackTraceRegex.matchAsPrefix(line);
    if (match == null) {
      return false;
    }
    return match.group(2)!.startsWith('package:logger');
  }

  bool _discardWebStacktraceLine(String line) {
    var match = _webStackTraceRegex.matchAsPrefix(line);
    if (match == null) {
      return false;
    }
    return match.group(1)!.startsWith('packages/logger') ||
        match.group(1)!.startsWith('dart-sdk/lib');
  }

  bool _discardBrowserStacktraceLine(String line) {
    var match = _browserStackTraceRegex.matchAsPrefix(line);
    if (match == null) {
      return false;
    }
    return match.group(1)!.startsWith('package:logger') ||
        match.group(1)!.startsWith('dart:');
  }

  String getTime() =>
      DateFormat('yyyy-MM-dd HH:mm:ss.SSS').format(DateTime.now());

  AnsiColor _getLevelColor(LoggerLevel level) {
    if (colors) {
      return levelColors[level]!;
    } else {
      return AnsiColor.none();
    }
  }

  String _getEmoji(LoggerLevel level) {
    if (printEmojis) {
      return levelEmojis[level]!;
    } else {
      return '';
    }
  }

  List<String> _formatAndPrint(
    String label,
    LoggerLevel level,
    String message,
    String? time,
    String? error,
    String? stacktrace,
  ) {
    // This code is non trivial and a type annotation here helps understanding.
    // ignore: omit_local_variable_types
    List<String> buffer = [];
    var verticalLineAtLevel =
        (includeBox[level]!) ? (label + verticalLine) : label;
    var color = _getLevelColor(level);
    if (includeBox[level]!) buffer.add(color(label + _topBorder));

    if (error != null) {
      var errorColor = _getLevelColor(LoggerLevel.ERROR);
      for (var line in error.split('\n')) {
        buffer.add(
          color(verticalLineAtLevel) +
              errorColor.resetForeground +
              errorColor(line) +
              errorColor.resetBackground,
        );
      }
      if (includeBox[level]!) buffer.add(color(label + _middleBorder));
    }

    if (stacktrace != null) {
      for (var line in stacktrace.split('\n')) {
        buffer.add(color('$verticalLineAtLevel$line'));
      }
      if (includeBox[level]!) buffer.add(color(label + _middleBorder));
    }

    if (time != null) {
      buffer.add(color('$verticalLineAtLevel$time'));
      if (includeBox[level]!) buffer.add(color(label + _middleBorder));
    }

    var emoji = _getEmoji(level);
    for (var line in message.split('\n')) {
      final segments = segmentMessage(line);
      for (int index = 0; index < segments.length; index++) {
        if (index == 0) {
          buffer.add(color("$verticalLineAtLevel$emoji${segments[index]}"));
        } else {
          buffer.add(color("$verticalLineAtLevel${segments[index]}"));
        }
      }
    }
    if (includeBox[level]!) buffer.add(color(label + _bottomBorder));
    return buffer;
  }

  List<String> segmentMessage(String line) {
    List<String> result = [];
    if (line.length <= limitLength) {
      result.add(line);
    } else {
      int segmentLine = 0;
      int start;
      int end;
      final int length = line.length;
      for (;;) {
        if (segmentLine * limitLength >= length) {
          break;
        }
        start = segmentLine * limitLength;
        end = start + min(length - start, limitLength);
        result.add(line.substring(start, end));
        if (end >= length) {
          break;
        }
        segmentLine++;
      }
    }
    return result;
  }
}
